在了解了物件導向的基礎後,接下來要討論的是當我們呼叫函式透過參數來傳遞資料時需要注意的一個細節,傳值 (Pass by value) 與傳址 (Pass by reference)。
首先,在開始討前,先讓我們了解一下上面提到的值 (value) 跟址 (reference) 是什麼。它們都是構成變數的一部分:值指的是變數所儲存的資料;而址則是指變數實際儲存資料的記憶體位置,在早期的程式話言中則被稱為指標 (Pointer)。為了方便理解,如果我們以容器的概念來比喻變數中值跟址的關係的話,那麼值便是容器中的內容物,而址則是容器本身。
那麼傳值跟傳址又是什麼意思呢?這就要提到函式中的參數了。我們都已經知道,由於變數在存取上會受到作用域的限制而使我們很多時候不能在函式中使用函式外的變數。因此我們需要透過函式的參數來突破作用域的限制,讓變數可以參與到函式的運作中。可是,這時候便衍生了另一個問題,那就是如果我們改動了函式參數的值的話,變數會受到影響嗎?答案是可能會,也可能不會,而決定這個答案的便是傳值跟傳址了。
當我們把變數透過函式參數來傳到函式中時,如果我們只傳了變數的值的話,無論參數有什麼改動,變數都不會有什麼影響。因為對於程式來說,我們是把變數這個「容器」中的內容物 (值) 的複製品 (Clone) 放到參數這個「另一個容器」中。因此,即使變數跟參數的值雖然看起來一樣,但它們實際上已經是程式中兩個完全獨立的內容物 (值),所以互相不會有任何的影響。
而當我們把變數透過函式參數來傳到函式中時,如果我們傳的是變數的址的話,變數便會跟著參數的改動一同變化。因為對於程式來說,傳變數的址的意思便是把變數這個「容器」當成參數。因此,當我們在函式中修改參數時,我們等於是在對變數本身進行修改。
那我們什麼時候會是傳值,什麼時候會是傳址呢?對於大多數的程式語言來說,參數是非物件資料型態的變數時便是傳值,而參數是物件資料型態的變數時便是傳址,當然,也有一部分的程式語言的判別方式是不一樣的,如沒有物件導向概念的 C 語言便是以指標 (Pointer)來決定函式是傳值還是傳址。因此,大家在使用程式語言時不妨可以注意一下傳值與傳址的問題,來避免出現非預期的改變變數或預期改變變數但卻沒有成功改變的狀況。
以下是傳值與傳址的例子:[C#]
using System;
public class Number { // 數字類別
public int val;
public Number(int _val) { // 建構子
val = _val;
}
}
public class PassExample
{
public static void swapValue(int a, int b) { // 使用 int 資料型態作為參數的函式,由於 int 資料型態不會建立物件,因此這函式傳遞資料的方式屬於傳值
int temp = a; // 宣告新的變數 temp 來儲存參數 a 的值
a = b; // 讓參數 a 的值儲存參數 b 的值
b = temp; // 讓參數 b 的值儲存參數 temp 的值
// P.S. 簡單的說這函式是讓參數 a 跟參數 b 互換成對方的值
}
public static void swapReference(Number a, Number b) { // 使用 Number 類別作為參數的函式,由於 Number 類別會建立物件,因此這函式傳遞資料的方式屬於傳址
int temp = a.val; // 宣告新的變數 temp 來儲存參數 a 的變數 val 的值
a.val = b.val; // 讓參數 a 的變數 val 的值儲存參數 b 的變數 val 的值
b.val = temp; // 讓參數 b 的變數 val 的值儲存參數 temp 的值
// P.S. 簡單的說這函式也是讓參數 a 跟參數 b 互換成對方的值,只是由於是物件,因此實際互換值的是它們的變數
}
public static void Main(string[] args)
{
int intA = 10, intB = 20;
Number numA = new Number(10);
Number numB = new Number(20);
Console.WriteLine("交換值前: a = " + intA + ", b = " + intB); // 顯示:交換值前: a = 10, b = 20
swapValue(intA, intB); // 以傳值的方式進行交換值的動作
Console.WriteLine("交換值後: a = " + intA + ", b = " + intB); // 顯示:交換值後: a = 10, b = 20
Console.WriteLine("交換值前: a = " + numA.val + ", b = " + numB.val); // 顯示:交換值前: a = 10, b = 20
swapReference(numA, numB); // 以傳址的方式進行交換值的動作
Console.WriteLine("交換值後: a = " + numA.val + ", b = " + numB.val); // 顯示:交換值後: a = 20, b = 10
}
}
當我們簡單的了解到物件 = 傳址、非物件 = 傳值
的基本想法時,我們會發現有一個程式語言,JavaScript,便推翻了這個想法:[JavaScript]
function changeVal(num){ // 傅遞物件參數的函式
num = { val: 20 }; // 改變物件參數的值
}
var number = { val: 10 }; // 宣告物件變數 number
console.log(number); // 顯示:{ val: 10 }
changeVal(number); // 把物件變數 number 傳到函式中
console.log(number); // 顯示:{ val: 10 }
我們會發現 JavaScript 中我們把物件傳到函式中卻沒有改變到物件的值,那是不是代表 JavaScript 中只有傳值而沒有傳址呢?讓我們把上面的例子稍微改變一下:
function changeVal(num){ // 傅遞物件參數的函式
num.val = 20; // 改變物件中的變數的值
}
var number = { val: 10 }; // 宣告物件變數 number
console.log(number); // 顯示:{ val: 10 }
changeVal(number); // 把物件變數 number 傳到函式中
console.log(number); // 顯示:{ val: 20 }
我們會發現在這個例子中 JavaScript 的函式確實是以傳址的方式改變了物件變數的值,這是怎麼回事呢?
在 JavaScript 中,對於物件變數的處理,在進行賦值時是以傳值的方式改變它的值,而改變物件中的變數的值時是以傳址的方式改變它的值。這種同時混合了傳值與傳址的資料傳遞方式便被視為第三種傳遞方式,被稱為 Pass by sharing。由於筆者對 JavaScript 的特質並不熟悉,沒辦法在這裡更深入的討論更多關於它的資料傳遞方式的詳情。如果大家對 Pass by sharing 感興趣的話,可以到下方的延伸閱讀中了解更多。
延伸閱讀:
你不可不知的 JavaScript 二三事#Day26:程式界的哈姆雷特 —— Pass by value, or Pass by reference? - https://ithelp.ithome.com.tw/articles/10209104